<?php

namespace PhpYes\Modules\Uri;


abstract class Uri
{
    const MainComponentScheme = 0;
    const MainComponentAuthority = 1;
    const MainComponentPath = 2;
    const MainComponentQuery = 3;
    const MainComponentFragment = 4;

    const HostTypeUnknown = 0;
    const HostTypeRegName = 1;
    const HostTypeIPv4Address = 2;
    const HostTypeIPv6Address = 3;
    const HostTypeIPvFuture = 4;

    public static $genDelimiters = [':', '/', '?', '#', '[', ']', '@'];
    public static $subDelimiters = ['!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '='];

    protected static $reservedCharacters = [':', '/', '?', '#', '[', ']', '@', '!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '='];
    protected static $reservedCharactersPct = ['3A', '2F', '3F', '23', '5B', '5D', '40', '21', '24', '26', '27', '28', '29', '2A', '2B', '2C', '3B', '3D'];

    protected static $unreservedCharacters = [
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        '-', '.', '_', '~'
    ];
    protected static $unreservedCharactersPct = [
        '30', '31', '32', '33', '34', '35', '36', '37', '38', '39',
        '41', '42', '43', '44', '45', '46', '47', '48', '49', '4A', '4B', '4C', '4D', '4E', '4F', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5A',
        '61', '62', '63', '64', '65', '66', '67', '68', '69', '6A', '6B', '6C', '6D', '6E', '6F', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7A',
        '2D', '2E', '5F', '7E'
    ];

    protected static $extraCharacters = [' ', '"', '%', '<', '>', '\\', '^', '`', '|', '{', '}'];
    protected static $extraCharactersPct = ['20', '22', '25', '3C', '3E', '5C', '5E', '60', '7C', '7B', '7D'];

    // /** @var string */
    // protected $scheme;
    /** @var string */
    protected $authority;
    /** @var string */
    protected $path;
    /** @var string */
    protected $query;
    /** @var string */
    protected $fragment;

    /** @var string */
    protected $userInfo;
    /** @var int */
    protected $hostType;
    /** @var string */
    protected $host;
    /** @var int */
    protected $port;

    /** @var string */
    protected $normalizeUri;

    protected function clearMemberVariables()
    {
        $this->authority = '';
        $this->path = '';
        $this->query = '';
        $this->fragment = '';

        $this->userInfo = '';
        $this->hostType = self::HostTypeUnknown;
        $this->host = '';
        $this->port = 0;

        $this->normalizeUri = '';
    }

    /**
     * @return string
     */
    public abstract function getScheme();

    /**
     * @return string
     */
    public function getAuthority()
    {
        return $this->authority;
    }

    /**
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * @return string
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * @return string
     */
    public function getFragment()
    {
        return $this->fragment;
    }

    /**
     * @return string
     */
    public function getUserInfo()
    {
        return $this->userInfo;
    }

    /**
     * @return int
     */
    public function getHostType()
    {
        return $this->hostType;
    }

    /**
     * @return string
     */
    public function getHost()
    {
        return $this->host;
    }

    /**
     * @return int
     */
    public function getPort()
    {
        return $this->port;
    }

    /**
     * @return string
     */
    public function getNormalizeUri()
    {
        return $this->normalizeUri;
    }

    /**
     * @param string $uri
     * @return Uri
     * @throws \Exception
     */
    public static function parseUri($uri)
    {
        if (empty($uri)) {
            throw new \Exception('error argument uri, uri should not be empty.');
        }

        $pos = strpos($uri, ':');
        if ($pos === false || $pos < 1) {
            throw new \Exception('error argument uri:' . $uri . ', uri is not valid.');
        }
        $scheme = strtolower(substr($uri, 0, $pos));

        $schemeImplemented = [
            'http' => 'PhpYes\Modules\Uri\HttpUri',
            'https' => 'PhpYes\Modules\Uri\HttpsUri',
        ];

        if (!isset($schemeImplemented[$scheme])) {
            throw new \Exception('error argument uri:' . $uri . ', scheme:' . $scheme . ' is unknown');
        }

        $class = $schemeImplemented[$pos];
        /** @var Uri $uriObj */
        $uriObj = new $class();
        $uriObj->parse($uri);

        return $uriObj;
    }

    /**
     * @param string $scheme
     * @return bool
     */
    public static function checkScheme($scheme)
    {
        // scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )

        if (empty($scheme)) {
            return false;
        }

        $firstChar = $scheme[0];
        if (!ctype_alpha($firstChar)) {
            return false;
        }

        $ss = str_replace(['+', '-', '.'], '', $scheme);
        if (!ctype_alnum($ss)) {
            return false;
        }

        return true;
    }

    /**
     * @param string $userInfo
     * @return bool
     */
    public static function checkUserInfo($userInfo)
    {
        // userinfo      = *( unreserved / pct-encoded / sub-delims / ":" )

        if (empty($userInfo)) {
            return true;
        }

        $len = strlen($userInfo);
        $pos = 0;
        while ($pos < $len) {
            $c = $userInfo[$pos];
            if ($c == '%') {
                if ($pos + 2 >= $len) {
                    return false;
                }
                $c = substr($userInfo, $pos, 3);
                if (!self::checkPctEncoded($c)) {
                    return false;
                }
                $pos += 2;
            } else {
                if (!(self::checkCharUnreserved($c) || in_array($c, self::$subDelimiters) || $c == ':')) {
                    return false;
                }
            }

            $pos += 1;
        }

        return true;
    }

    /**
     * @param string $regName
     * @return bool
     */
    public static function checkRegName($regName)
    {
        // reg-name      = *( unreserved / pct-encoded / sub-delims )

        if (empty($regName)) {
            return true;
        }

        $len = strlen($regName);
        $pos = 0;
        while ($pos < $len) {
            $c = $regName[$pos];
            if ($c == '%') {
                if ($pos + 2 >= $len) {
                    return false;
                }
                $c = substr($regName, $pos, 3);
                if (!self::checkPctEncoded($c)) {
                    return false;
                }
                $pos += 2;
            } else {
                if (!(self::checkCharUnreserved($c) || in_array($c, self::$subDelimiters))) {
                    return false;
                }
            }

            $pos += 1;
        }

        return true;
    }

    /**
     * @param string $ip
     * @return bool
     */
    public static function checkIPv4Address($ip)
    {
        // IPv4address   = dec-octet "." dec-octet "." dec-octet "." dec-octet
        // dec-octet     = DIGIT                 ; 0-9
        //               / %x31-39 DIGIT         ; 10-99
        //               / "1" 2DIGIT            ; 100-199
        //               / "2" %x30-34 DIGIT     ; 200-249
        //               / "25" %x30-35          ; 250-255

        if (empty($ip)) {
            return false;
        }

        $decOctetArr = explode('.', $ip);
        $cnt = count($decOctetArr);
        if ($cnt != 4) {
            return false;
        }

        foreach ($decOctetArr as $decOctet) {
            $len = strlen($decOctet);
            if ($len < 1 || $len > 3) {
                return false;
            }
            if (!ctype_digit($decOctet)) {
                return false;
            }

            $num = intval($decOctet);
            if ($num < 0 || $num > 255) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param string $ip
     * @return bool
     */
    public static function checkIPv6Address($ip)
    {
        // IPv6address   =                            6( h16 ":" ) ls32 ; 1
        //               /                       "::" 5( h16 ":" ) ls32 ; 2
        //               / [               h16 ] "::" 4( h16 ":" ) ls32 ; 3
        //               / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 ; 4
        //               / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 ; 5
        //               / [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32 ; 6
        //               / [ *4( h16 ":" ) h16 ] "::"              ls32 ; 7
        //               / [ *5( h16 ":" ) h16 ] "::"              h16  ; 8
        //               / [ *6( h16 ":" ) h16 ] "::"                   ; 9

        // h16           = 1*4HEXDIG
        // ls32          = ( h16 ":" h16 ) / IPv4address

        if (empty($ip)) {
            return false;
        }
        $ipLen = strlen($ip);
        if ($ipLen < 2) {
            return false;
        } elseif ($ipLen == 2) {
            if ($ip == '::') {
                return true;
            } else {
                return false;
            }
        }
        // ipLen > 2

        $pos = strpos($ip, '::');
        if ($pos === false) { // 1
            $pieceArr = explode(':', $ip);
            $cnt = count($pieceArr);
            if ($cnt == 7) {
                if (!self::checkIPv4Address($pieceArr[6])) {
                    return false;
                }
                $hCnt = 6;
            } elseif ($cnt == 8) {
                $hCnt = 8;
            } else {
                return false;
            }
            for ($i = 0; $i < $hCnt; $i++) {
                $piece = $pieceArr[$i];
                $len = strlen($piece);
                if ($len < 1 || $len > 4) {
                    return false;
                }
                if (!ctype_xdigit($piece)) {
                    return false;
                }
            }
        } elseif ($pos == 0) { // 2
            $r = substr($ip, 2);
            $pieceArr = explode(':', $r);
            $cnt = count($pieceArr);
            $lastPiece = $pieceArr[$cnt - 1];
            if (self::checkIPv4Address($lastPiece)) {
                $hCnt = $cnt - 1;
                if ($hCnt > 5) {
                    return false;
                }
            } else {
                $hCnt = $cnt;
                if ($hCnt > 7) {
                    return false;
                }
            }
            for ($i = 0; $i < $hCnt; $i++) {
                $piece = $pieceArr[$i];
                $len = strlen($piece);
                if ($len < 1 || $len > 4) {
                    return false;
                }
                if (!ctype_xdigit($piece)) {
                    return false;
                }
            }
        } else {
            $l = substr($ip, 0, $pos);
            if ($ipLen == $pos + 2) {
                $r = '';
            } else {
                $r = substr($ip, $pos + 2);
            }
            if (empty($r)) { // 9
                $lmCnt = 7;
            } else {
                $pieceArr = explode(':', $r);
                $cnt = count($pieceArr);
                $lastPiece = $pieceArr[$cnt - 1];
                if (self::checkIPv4Address($lastPiece)) {
                    $hCnt = $cnt - 1;
                    if ($hCnt > 4) {
                        return false;
                    }
                    $lmCnt = 6 - $cnt;
                } else {
                    $hCnt = $cnt;
                    if ($hCnt > 6) {
                        return false;
                    }
                    $lmCnt = 7 - $cnt;
                }
                for ($i = 0; $i < $hCnt; $i++) {
                    $piece = $pieceArr[$i];
                    $len = strlen($piece);
                    if ($len < 1 || $len > 4) {
                        return false;
                    }
                    if (!ctype_xdigit($piece)) {
                        return false;
                    }
                }
            }

            $pieceArr = explode(':', $l);
            $cnt = count($pieceArr);
            if ($cnt > $lmCnt) {
                return false;
            }

            foreach ($pieceArr as $piece) {
                $len = strlen($piece);
                if ($len < 1 || $len > 4) {
                    return false;
                }
                if (!ctype_xdigit($piece)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @param string $ip
     * @return bool
     */
    public static function checkIPvFuture($ip)
    {
        // IPvFuture     = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )

        if (empty($ip)) {
            return false;
        }

        $len = strlen($ip);
        if ($len < 4) {
            return false;
        }

        if (!($ip[0] == 'v' || $ip[0] == 'V')) {
            return false;
        }
        if (!ctype_xdigit($ip[1])) {
            return false;
        }
        if ($ip[2] != '.') {
            return false;
        }

        $pos = 3;
        while ($pos < $len) {
            $c = $ip[$pos];
            if (!(self::checkCharUnreserved($c) || in_array($c, self::$subDelimiters) || $c == ':')) {
                return false;
            }
            $pos += 1;
        }

        return true;
    }

    /**
     * @param int $port
     * @return bool
     */
    public static function checkPort($port)
    {
        // port          = *DIGIT

        if (empty($port)) {
            return true;
        }

        return ctype_digit($port);
    }

    /**
     * @param string $path
     * @return bool
     */
    public static function checkPathAbempty($path)
    {
        // path          = path-abempty    ; begins with "/" or is empty
        //               / path-absolute   ; begins with "/" but not "//"
        //               / path-noscheme   ; begins with a non-colon segment
        //               / path-rootless   ; begins with a segment
        //               / path-empty      ; zero characters

        // path-abempty  = *( "/" segment )

        if (empty($path)) {
            return true;
        }

        if ($path[0] != '/') {
            return false;
        }

        $segmentArr = explode('/', $path);

        foreach ($segmentArr as $segment) {
            if (!self::checkSegment($segment)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param string $path
     * @return bool
     */
    public static function checkPathAbsolute($path)
    {
        // path          = path-abempty    ; begins with "/" or is empty
        //               / path-absolute   ; begins with "/" but not "//"
        //               / path-noscheme   ; begins with a non-colon segment
        //               / path-rootless   ; begins with a segment
        //               / path-empty      ; zero characters

        // path-absolute = "/" [ segment-nz *( "/" segment ) ]

        if (empty($path)) {
            return false;
        }

        if ($path[0] != '/') {
            return false;
        }

        if (strlen($path) == 1) {
            return true;
        }

        $segmentArr = explode('/', $path);
        if (!self::checkSegmentNz($segmentArr[1])) {
            return false;
        }
        $cnt = count($segmentArr);
        if ($cnt == 2) {
            return true;
        }

        for ($i = 2; $i < $cnt; $i++) {
            if (!self::checkSegment($segmentArr[$i])) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param string $path
     * @return bool
     */
    public static function checkPathNoscheme($path)
    {
        // path          = path-abempty    ; begins with "/" or is empty
        //               / path-absolute   ; begins with "/" but not "//"
        //               / path-noscheme   ; begins with a non-colon segment
        //               / path-rootless   ; begins with a segment
        //               / path-empty      ; zero characters

        // path-noscheme = segment-nz-nc *( "/" segment )

        if (empty($path)) {
            return false;
        }

        $segmentArr = explode('/', $path);
        if (!self::checkSegmentNzNc($segmentArr[0])) {
            return false;
        }

        $cnt = count($segmentArr);
        if ($cnt == 1) {
            return true;
        }

        for ($i = 1; $i < $cnt; $i++) {
            if (!self::checkSegment($segmentArr[$i])) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param string $path
     * @return bool
     */
    public static function checkPathRootless($path)
    {
        // path          = path-abempty    ; begins with "/" or is empty
        //               / path-absolute   ; begins with "/" but not "//"
        //               / path-noscheme   ; begins with a non-colon segment
        //               / path-rootless   ; begins with a segment
        //               / path-empty      ; zero characters

        // path-rootless = segment-nz *( "/" segment )

        if (empty($path)) {
            return false;
        }

        $segmentArr = explode('/', $path);
        if (!self::checkSegmentNz($segmentArr[0])) {
            return false;
        }

        $cnt = count($segmentArr);
        if ($cnt == 1) {
            return true;
        }

        for ($i = 1; $i < $cnt; $i++) {
            if (!self::checkSegment($segmentArr[$i])) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param string $segment
     * @return bool
     */
    public static function checkSegment($segment)
    {
        // segment       = *pchar

        if (empty($segment)) {
            return true;
        }

        $len = strlen($segment);
        $pos = 0;
        while ($pos < $len) {
            $pchar = $segment[$pos];
            if ($pchar == '%') {
                if ($pos + 2 >= $len) {
                    return false;
                }
                $pchar = substr($segment, $pos, 3);
                $pos += 2;
            }
            if (!self::checkPchar($pchar)) {
                return false;
            }
            $pos += 1;
        }

        return true;
    }

    /**
     * @param string $segment
     * @return bool
     */
    public static function checkSegmentNz($segment)
    {
        // segment-nz    = 1*pchar

        if (empty($segment)) {
            return false;
        }

        return self::checkSegment($segment);
    }

    /**
     * @param string $segment
     * @return bool
     */
    public static function checkSegmentNzNc($segment)
    {
        // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )

        if (empty($segment)) {
            return false;
        }

        $len = strlen($segment);
        $pos = 0;
        while ($pos < $len) {
            $nc = $segment[$pos];
            if ($nc == '%') {
                if ($pos + 2 >= $len) {
                    return false;
                }
                $nc = substr($segment, $pos, 3);
                if (!self::checkPctEncoded($nc)) {
                    return false;
                }
                $pos += 2;
            } else {
                if (!(self::checkCharUnreserved($nc) || in_array($nc, self::$subDelimiters) || $nc == '@')) {
                    return false;
                }
            }

            $pos += 1;
        }

        return true;
    }

    /**
     * @param string $query
     * @return bool
     */
    public static function checkQuery($query)
    {
        // query         = *( pchar / "/" / "?" )

        if (empty($query)) {
            return true;
        }

        $len = strlen($query);
        $pos = 0;
        while ($pos < $len) {
            $pchar = $query[$pos];
            if ($pchar == '%') {
                if ($pos + 2 >= $len) {
                    return false;
                }
                $pchar = substr($query, $pos, 3);
                $pos += 2;
            }
            if (!(self::checkPchar($pchar) || $pchar == '/' || $pchar == '?')) {
                return false;
            }
            $pos += 1;
        }

        return true;
    }

    /**
     * @param string $fragment
     * @return bool
     */
    public static function checkFragment($fragment)
    {
        // fragment      = *( pchar / "/" / "?" )
        // 可以看出 fragment 同 query 一样
        return self::checkQuery($fragment);
    }

    /**
     * @param string $pchar - 单个字符或者%编码的字符(%xx)
     * @return bool
     */
    public static function checkPchar($pchar)
    {
        // pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"

        if (empty($pchar)) {
            return false;
        }

        $len = strlen($pchar);

        if ($len == 1) {
            if (self::checkCharUnreserved($pchar) || in_array($pchar, self::$subDelimiters) || $pchar == ':' || $pchar == '@') {
                return true;
            }
        } elseif ($len == 3) {
            return self::checkPctEncoded($pchar);
        } else {
            return false;
        }

        return false;
    }

    /**
     * @param string $pctXx - %编码的字符(%xx)
     * @return bool
     */
    public static function checkPctEncoded($pctXx)
    {
        // pct-encoded   = "%" HEXDIG HEXDIG

        if (empty($pctXx) || strlen($pctXx) != 3) {
            return false;
        }

        if ($pctXx[0] == '%' && ctype_xdigit($pctXx[1]) && ctype_xdigit($pctXx[2])) {
            return true;
        }

        return false;
    }

    /**
     * @param string $char - 单个字符
     * @return bool
     */
    public static function checkCharUnreserved($char)
    {
        // unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"

        if (empty($char)) {
            return false;
        }

        if (ctype_alnum($char) || in_array($char, ['-', '.', '_', '~'])) {
            return true;
        }

        return false;
    }

    /**
     * @param string $char - 单个字符
     * @return bool
     */
    public static function checkCharReserved($char)
    {
        // reserved      = gen-delims / sub-delims

        if (empty($char)) {
            return false;
        }

        if (in_array($char, self::$genDelimiters) || in_array($char, self::$subDelimiters)) {
            return true;
        }

        return false;
    }

    /**
     * @param string $path
     * @return string
     * @throws \Exception
     */
    public static function removeDotSegments($path)
    {
        if (empty($path)) {
            return '';
        }

        $outArr = array();
        $segmentArr = explode('/', $path);
        foreach ($segmentArr as $segment) {
            if ($segment == '.') {
                continue;
            } elseif ($segment == '..') {
                if (empty($outArr)) {
                    throw new \Exception('error path: ' . $path);
                }
                array_pop($outArr);
            } else {
                array_push($outArr, $segment);
            }
        }

        return implode('/', $outArr);
    }

    /**
     * @param string $in
     * @return string
     */
    public static function pctEncode($in)
    {
        if (empty($in)) {
            return '';
        }

        $out = '';
        $len = strlen($in);
        for ($i = 0; $i < $len; $i++) {
            $c = $in[$i];
            $idx = array_search($c, self::$reservedCharacters);
            if ($idx === false) {
                $out .= $c;
            } else {
                $out .= '%' . self::$reservedCharactersPct[$idx];
            }
        }

        return $out;
    }

    /**
     * @param string $in
     * @return string
     * @throws \Exception
     */
    public static function decodePct($in)
    {
        if (empty($in)) {
            return '';
        }

        $out = '';
        $len = strlen($in);
        for ($i = 0; $i < $len; $i++) {
            $c = $in[$i];
            if ($c == '%') {
                $i += 2;
                if ($i >= $len) {
                    throw new \Exception('error argument: ' . $in);
                }
                $xx = strtoupper(substr($in, $i - 1, 2));
                $idx = array_search($xx, self::$reservedCharactersPct);
                if ($idx !== false) {
                    $out .= self::$reservedCharacters[$idx];
                    continue;
                }
                $idx = array_search($xx, self::$unreservedCharactersPct);
                if ($idx !== false) {
                    $out .= self::$unreservedCharacters[$idx];
                    continue;
                }
                $idx = array_search($xx, self::$extraCharactersPct);
                if ($idx !== false) {
                    $out .= self::$extraCharacters[$idx];
                    continue;
                }
                throw new \Exception('percent-encoded octet: %' . $xx . ' not supported');
            } else {
                $out .= $c;
            }
        }

        return $out;
    }

    /**
     * @param string $scheme
     * @return string
     */
    public static function normalizeScheme($scheme)
    {
        if (empty($scheme)) {
            return '';
        }

        return strtolower($scheme);
    }

    /**
     * @param int $hostType
     * @param string $host
     * @return string
     * @throws \Exception
     */
    public static function normalizeHost($hostType, $host)
    {
        if (empty($host)) {
            return '';
        }

        if ($hostType != self::HostTypeRegName) {
            return strtolower($host);
        }

        $out = '';
        $len = strlen($host);
        for ($i = 0; $i < $len; $i++) {
            $c = $host[$i];
            if ($c == '%') {
                $i += 2;
                if ($i >= $len) {
                    throw new \Exception('error host: ' . $host);
                }
                $xx = strtoupper(substr($host, $i - 1, 2));
                $idx = array_search($xx, self::$reservedCharactersPct);
                if ($idx !== false) {
                    $out .= '%' . $xx;
                    continue;
                }
                $idx = array_search($xx, self::$unreservedCharactersPct);
                if ($idx !== false) {
                    $out .= self::$unreservedCharacters[$idx];
                    continue;
                }
                $idx = array_search($xx, self::$extraCharactersPct);
                if ($idx !== false) {
                    $out .= self::$extraCharacters[$idx];
                    continue;
                }
                throw new \Exception('percent-encoded octet: %' . $xx . ' not supported');
            } else {
                $out .= strtolower($c);
            }
        }

        return $out;
    }

    /**
     * @param string $in
     * @return string
     * @throws \Exception
     */
    public static function normalizePctTriplet($in)
    {
        if (empty($in)) {
            return '';
        }

        $out = '';
        $len = strlen($in);
        for ($i = 0; $i < $len; $i++) {
            $c = $in[$i];
            if ($c == '%') {
                $i += 2;
                if ($i >= $len) {
                    throw new \Exception('error argument: ' . $in);
                }
                $xx = strtoupper(substr($in, $i - 1, 2));
                $idx = array_search($xx, self::$reservedCharactersPct);
                if ($idx !== false) {
                    $out .= '%' . $xx;
                    continue;
                }
                $idx = array_search($xx, self::$unreservedCharactersPct);
                if ($idx !== false) {
                    $out .= self::$unreservedCharacters[$idx];
                    continue;
                }
                $idx = array_search($xx, self::$extraCharactersPct);
                if ($idx !== false) {
                    $out .= self::$extraCharacters[$idx];
                    continue;
                }
                throw new \Exception('percent-encoded octet: %' . $xx . ' not supported');
            } else {
                $out .= $c;
            }
        }

        return $out;
    }

    /**
     * @throws \Exception
     */
    protected function parseAuthority()
    {
        if (empty($this->authority)) {
            $this->hostType = self::HostTypeRegName;

            return;
        }

        $pos = strpos($this->authority, '@');
        if ($pos === false) {
            $hostPort = $this->authority;
        } elseif ($pos == 0) {
            if (strlen($this->authority) == 1) {
                throw new \Exception('错误的authority');
            }
            $this->userInfo = '';
            $hostPort = substr($this->authority, 1);
        } else {
            if ($pos + 1 == strlen($this->authority)) {
                throw new \Exception('错误的authority');
            }
            $userInfo = substr($this->authority, 0, $pos);
            if (!self::checkUserInfo($userInfo)) {
                throw new \Exception('错误的userInfo');
            }
            $this->userInfo = $userInfo;
            $hostPort = substr($this->authority, $pos + 1);
        }

        if ($hostPort[0] == '[') {
            $pos = strpos($hostPort, ']');
            if ($pos === false || $pos == 1) {
                throw new \Exception('错误的host');
            }
            $len = strlen($hostPort);
            if ($pos + 1 == $len) {
                $this->port = $this->getDefaultPort();
            } else {
                if ($hostPort[$pos + 1] != ':') {
                    throw new \Exception('port应该以:开始');
                }
                if ($pos + 2 == $len) {
                    $this->port = $this->getDefaultPort();
                } else {
                    $port = substr($hostPort, $pos + 2);
                    if (!self::checkPort($port)) {
                        throw new \Exception('错误的port');
                    }
                    $this->port = intval($port);
                }
            }

            $host = substr($hostPort, 1, $pos - 1);
            if (self::checkIPv6Address($host)) {
                $this->hostType = self::HostTypeIPv6Address;
            } elseif (self::checkIPvFuture($host)) {
                $this->hostType = self::HostTypeIPvFuture;
            } else {
                throw new \Exception('错误的host');
            }
            $this->host = $host;
        } else {
            $pos = strpos($hostPort, ':');
            if ($pos === false) {
                $host = $hostPort;
                $this->port = $this->getDefaultPort();
            } elseif ($pos == 0) {
                throw new \Exception('错误的host');
            } else {
                $host = substr($hostPort, 0, $pos);
                if ($pos + 1 == strlen($hostPort)) {
                    $this->port = $this->getDefaultPort();
                } else {
                    $port = substr($hostPort, $pos + 1);
                    if (!self::checkPort($port)) {
                        throw new \Exception('错误的port');
                    }
                    $this->port = intval($port);
                }
            }

            if (self::checkRegName($host)) {
                $this->hostType = self::HostTypeRegName;
            } elseif (self::checkIPv4Address($host)) {
                $this->hostType = self::HostTypeIPv4Address;
            } else {
                throw new \Exception('错误的host');
            }
            $this->host = $host;
        }
    }

    /**
     * @param string $uri
     * @return mixed
     * @throws \Exception
     */
    public abstract function parse($uri);

    /**
     * @param string $baseUri
     * @param string $relativeRef
     * @return mixed
     * @throws \Exception
     */
    public abstract function parseRelativeRef($baseUri, $relativeRef);

    /**
     * @return mixed
     * @throws \Exception
     */
    public abstract function normalize();

    /**
     * @return int
     */
    public abstract function getDefaultPort();
}